Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DFC Orders] Sync remote products when order cycle opens #13167

Open
wants to merge 15 commits into
base: master
Choose a base branch
from

Conversation

dacook
Copy link
Member

@dacook dacook commented Feb 19, 2025

ℹ️ Funded Feature. Please track ALL ASSOCIATED WORK under the associated tracking code #11678 DFC Orders

What? Why?

When an order cycle opens, any remote (DFC) products will get re-imported to ensure the product is up to date. This is because the product on OFN is considered a cache, or a view of, the "real" product.
There's currently no record or notification of this happening for the enterprise managers or OC co-ordinator.

This required some changes to the way we enqueue and execute the scheduled jobs for opening an order cycle.

There is also a slight change to the Webhook order_cycle.opened (https://ofn-user-guide.gitbook.io/ofn-api-handbook/webhooks/event-types). The at timestamp now more accurately reflects the time the event occurred, rather than when the webhook was sent. It's such a nuanced change I don't think it's necessary to announce.

What should we test?

  • Import a product from DFC (see example at [DFC Orders] List products to import on screen #13125 (comment))
    • Note: the product must be imported to an enterprise that you are the owner of.
  • Edit some of the details, so that it becomes out of sync with the original source. (eg variant name, price, etc)
  • Add the product to an order cycle and schedule it to open soon
  • After the order cycle is opened, the details should be updated.

Release notes

Changelog Category (reviewers may add a label for the release notes):

  • User facing changes
  • API changes (V0, V1, DFC or Webhook)
  • Technical changes only
  • Feature toggled

The title of the pull request will be included in the release notes.

@dacook dacook force-pushed the sync-products-on-oc-opened-12986 branch 2 times, most recently from 4d8cd8c to f665516 Compare February 19, 2025 04:22
@dacook dacook requested a review from mkllnk February 19, 2025 04:22
@dacook

This comment was marked as resolved.

Copy link
Member

@mkllnk mkllnk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, all going in the right direction. That's basically it. You just need to check for other open order cycles of the product, maybe. Or maybe we just skip that until it becomes a problem? I'm worried that multiple enterprises could have different overlapping OCs and then this will never get triggered. 🤷 But maybe that's a problem we don't have yet. 😉

@dacook dacook force-pushed the sync-products-on-oc-opened-12986 branch 2 times, most recently from ef5ce63 to 2d4c5d1 Compare February 24, 2025 00:20
def mark_as_opened(order_cycles)
now = Time.zone.now
order_cycles.update_all(opened_at: now, updated_at: now)
end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a spec for this in a later commit.

@dacook dacook force-pushed the sync-products-on-oc-opened-12986 branch 2 times, most recently from 06088fd to 42ddb61 Compare February 24, 2025 01:28
@dacook dacook changed the title Sync products when Order Cycle opens Sync products when order cycle opens Feb 24, 2025
@dacook dacook changed the title Sync products when order cycle opens Sync remote products when order cycle opens Feb 24, 2025
I considered pre-loading the variant and product with includes, But can't do that here because it's a polymorphic relationship.
It was probably better to be explicit at each test, but this one is always repeated and approaches the line length, so I wanted to just define it once at the top.
There might be a delay before it gets sent, so it's better to record the time the event occurred at.
It would have been simpler to just add it to the data hash, but I felt it was an important detail for an event and should be at the top level along with event name.

In the case of order cycle opening, this is the same as opened_at. I've included this in the payload for clarity too.
And re-organise specs.

TOFIX: concurrency test now fails. why?
Being based on the DB value should be more robust.

This prevents an order cycle from being "opened" when it's already open. But note that the order cycle can become "unopened" (see OrderCycle#reset_opened_at).

Nice to see the database query count drop, but I must confess I don't know why!
and in that case, might as well define it once at the top.

But it didn't help with spec failures.. see next commit
I guess we lose some of the detail in the db.
@dacook dacook force-pushed the sync-products-on-oc-opened-12986 branch from 166ed5c to 65d8c56 Compare February 24, 2025 03:03
@dacook dacook marked this pull request as ready for review February 24, 2025 03:03
@dacook dacook changed the title Sync remote products when order cycle opens [DFC Orders] Sync remote products when order cycle opens Feb 24, 2025
@dacook dacook requested a review from mkllnk February 25, 2025 03:08
@mkllnk mkllnk added the user facing changes Thes pull requests affect the user experience label Feb 25, 2025
Copy link
Member

@mkllnk mkllnk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. I lost some focus towards the end because it's after 5pm. But it's mostly good. I think there are a few small things to improve and then it's good.

Comment on lines 44 to 51
before do
user.oidc_account.update!(token: allow_token_for(email: user.email))
end

# should we move any parts of importing to a separate class, and test it separately?
it "synchronises products from a FDC catalog", vcr: true do
user.update!(oidc_account: build(:testdfc_account))
# One product is existing in OFN
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that you want to create(:testdfc_user). The allow_token_for method from the AuthorizationHelper is to create a valid access token locally for the OFN DFC API. If you use VCR to record a real interaction then you will have real tokens and don't need that helper.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, thanks.
I have to admit I cheated and copy/pasted the VCR fixture from elsewhere. I can't re-record it because I'm forbidden. Can you please guide me how to authenticate it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how to best document this. You need a refresh token. So the best way I found is to run your local dev server, connect a user to the testdfc user OIDC account and then get the token:

./bin/rails runner 'puts "OPENID_REFRESH_TOKEN=\"#{OidcAccount.last.refresh_token}\""'

You then paste that line into your .env.test.local file. You also need the OIDC creds in your env:

OPENID_APP_ID="coopcircuits"                                                    
OPENID_APP_SECRET="..."
OPENID_REFRESH_TOKEN="eyJh..."

I think that we should put these instructions as a comment in .env.test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! You've told me this before and I forgot sorry. I'll push up a suggested .env.test change.

class OpenOrderCycleJob < ApplicationJob
def perform(order_cycle_id)
order_cycle = OrderCycle.find(order_cycle_id)
sync_remote_variants(order_cycle)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagined the syncing in a new job. Then that logic is isolated and can be triggered via buttons or at other points, too. But I think that you wanted to make sure it finishes before the order cycle is seen as open. Fair enough. There is perform_now though which can be handy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it might be useful in the future but I didn't think it was needed yet.

Open Order Cycle still needs its own job here, so that the syncing (which has to happen synchronously) doesn't delay the opening of other order cycles.

The naming is confusingly similar to the existing OrderCycleOpenedJob, but let's see if we can improve that one..

# Currently, an order cycle is considered open in the shopfront when orders_open_at >= now.
# But now there are some pre-conditions for opening an order cycle, so we would like to change that.
# When it changes, this would become OrderCycleOpeningJob, with the responsibility of opening each
# order cycle by setting opened_at = now.
class OrderCycleOpenedJob < ApplicationJob
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name is now confusing because it happens after orders_open_at but before opened_at. So the OC hasn't really opened yet. Most of my ideas are still confusing. Maybe FindOrderCyclesToOpenJob? 🤷

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm not sure either.. how about:

 EnqueueOpeningOrderCyclesJob
ScheduleOpeningOrderCyclesJob
 ProcessOpeningOrderCyclesJob
        OpeningOrderCyclesJob

ScheduleOrderCyclesToOpenJob

I think I like the last one, what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the first and the last. I would go for Enqueue because it doesn't schedule for a future time. It just does it straight after. It could even happen in parallel because Sidekiq uses several threads. It's really hard to write though. Maybe trigger is a better word? In that case we could also say TriggerOrderCycleOpeningJob. But then it's better the re-use the other job name: TriggerOpenOrderCycleJob. I also like the ToOpen because it refers to the action to do. My favourites now are:

TriggerOpeningOrderCyclesJob
    TriggerOpenOrderCycleJob
 TriggerOrderCyclesToOpenJob

The last one is my favourite. But I would also be happy about ScheduleOrderCyclesToOpenJob if you prefer the schedule part.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Trigger sounds good, I'll go ahead with TriggerOrderCyclesToOpenJob

Comment on lines +22 to +24
.to change { order_cycle.opened_at }

expect(order_cycle.opened_at).to be_within(1).of(now)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I thought that there was some way to convert the timestamp to the db precision. I can't remember it now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this was just the first solution I came across, and it worked :)
Another thought was to use to_i when comparing.

dacook and others added 2 commits February 26, 2025 10:00
Co-authored-by: Maikel <[email protected]>
> The order cycle itself is not changed. It's just that time passed and it's now considered open/closed.
@dacook
Copy link
Member Author

dacook commented Feb 26, 2025

@mkllnk thanks for your feedback. I've made some changes and have a couple of questions.

@dacook dacook force-pushed the sync-products-on-oc-opened-12986 branch from 2eb96bc to e2a871e Compare February 26, 2025 00:08
@dacook dacook requested a review from mkllnk February 26, 2025 00:08
Comment on lines 22 to 23
rescue ActiveRecord::RecordNotFound
# do nothing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't really happen, should it? I'm wondering if we should report this to Bugsnag. Otherwise there may be a case when we are wondering: Why did the job not do its job? And we won't have any record of that.

I think that this only happens if the syncing takes so long that five minutes passed and the job gets scheduled again.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we shouldn't suppress the error.

Because Sidekiq retries jobs that raise an exception (for about 20 days), I chose to suppress the error.
I also don't want to set sidekiq_options retries: 0, in case there's some other error, and maybe it should be retried (although maybe only for 10 mins). Sidekiq doesn't seem to have a way to configure retries based on a condition.

So.. I'm thinking to set sidekiq_options retry_for: 10.minutes and not have any error handling. What do you think?

@dacook dacook force-pushed the sync-products-on-oc-opened-12986 branch from e2a871e to 0fa71e2 Compare February 26, 2025 04:39
After 10 minutes, I'd consider that it failed to open the order cycle. Who would want their products to sync, or get a notification at a random time during the order cycle?

Best viewed with whitespace ignored.
This name better reflects what it's doing.

As this job is scheduled automatically by Sidekiq, I think there shouldn't be any jobs with the old name in redis. So I didn't bother keeping a placholder for the old name.
@dacook dacook force-pushed the sync-products-on-oc-opened-12986 branch from f2a0a6d to 6d5aaa1 Compare February 26, 2025 05:09
@dacook dacook requested a review from mkllnk February 26, 2025 05:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
user facing changes Thes pull requests affect the user experience
Projects
Status: In Progress ⚙
Development

Successfully merging this pull request may close these issues.

[DFC Products] Ensure Product details are synced at correct intervals
2 participants